序言
从前文我们知道,在事务运行过程中,数据库设置的隔离级别不同,解决的并发问题也不同。
那么思考一个问题:这些隔离级别在内部到底是如何解决并发问题的呢?
在数据库系统中,是通过 MVCC 和锁机制来解决该问题的。
那么,下面来详细了解下它们吧!
MVCC
MVCC(Multi-Version Concurrency Control),即多版本并发控制,指在RC
、RR
两种隔离级别下,事务在执行普通的 SELECT 操作时访问记录的版本链的过程。
作用
MVCC 用于解决一些并发问题,最终提高并发情况的性能:
- 读-读冲突:在传统的锁机制下,多个事务可以同时读取同一行数据,但是如果有一个事务正在修改数据,则其他事务的读操作应该等待。MVCC 允许多个事务并发读取数据的不同版本,从而解决了读-读冲突的问题,提高了并发性能
- 读-写冲突:在传统的锁机制下,读操作和写操作会发生冲突,即一个事务正在写入数据时,其他事务无法读取该数据。这会导致并发性能下降,MVCC 通过允许读操作与写操作并发进行,解决了读-写冲突的问题
- 写-写冲突:传统的锁机制还会导致写操作之间的冲突,即多个事务同时试图修改同一行数据,只有一个事务能成功,其他事务需要等待。MVCC 通过使用版本号或时间戳来跟踪数据的不同版本,每个事务操作的是数据的一个特定版本,从而解决了写-写冲突的问题
MVCC 支持一致性读取,即读取操作会看到一个一致性的数据库快照。读操作会根据事务开始的时间或快照版本确定可见的数据版本,而不会受到后续并发事务的修改影响。这样可以保证读操作的一致性,避免了脏读或不可重复读等问题。
数据库差异
不仅是 MySQL,包括 Oracle、PostgreSQL 等其他数据库系统也都实现了 MVCC,但各自的实现机制不尽相同,因为 MVCC 没有一个统一的实现标准,典型的有乐观(optimistic)并发控制和悲观(pessimistic)并发控制。
原理
使用版本链。
版本链
版本链是数据修改时出现的对应数据的一串 undo-log 链表。
下面,我们通过一个例子了解它。
快速入门
对于使用InnoDB
存储引擎的表来说,它的聚簇索引记录中都包含两个必要的隐藏列:
trx_id
:每次对某条聚簇索引记录进行改动时,都会把对应的事务 id 赋值给trx_id
隐藏列。roll_pointer
:每次对某条聚簇索引记录进行改动时,都会把旧的版本写入到undo
日志中,该隐藏列相当于一个指针,通过它可以找到该记录修改前的信息。
下面的t
表现在只包含一条记录:
1 | mysql> SELECT * FROM t; |
假设插入该记录时的trx_id
(事务 id)为 80,那么此刻该条记录的示意图如下所示:
假设之后两个trx_id
分别为 100、200 的事务对这条记录进行 UPDATE
操作,操作流程如下表:
时间 | trx_id :100 |
trx_id :200 |
---|---|---|
T1 | BEGIN; |
|
T2 | BEGIN; |
|
T3 | UPDATE t SET u = '关羽' WHERE id = 1 |
|
T4 | UPDATE t SET u = '张飞' WHERE id = 1 |
|
T5 | COMMIT; |
|
T6 | UPDATE t SET u = '赵云' WHERE id = 1 |
|
T7 | UPDATE t SET u = '诸葛亮' WHERE id = 1 |
|
T8 | COMMIT; |
每次对记录进行改动,都会记录一条undo
日志,每条undo
日志也都有一个roll_pointer
属性,将这些undo
日志都连起来就形成了一个链表,所以现在的情况就像下图一样:
版本链的头节点存储了当前记录的最新值,每次对该记录更新后,都会将旧值放到一条undo
日志中,即该记录的一个旧版本。随着更新次数的增多,所有的版本都会被roll_pointer
属性连接成一个链表,这个链表就被称之为版本链。
另外,每个版本中还包含生成该版本时对应的trx_id
,这个信息很重要,因为我们稍后就会用到。
ReadView
对于不同隔离级别的事务来说,读取记录的方式也不同:
隔离级别 | 读取记录方式 |
---|---|
READ UNCOMMITTED |
直接读取记录的最新版本 |
READ COMMITTED |
使用版本链 |
REPEATABLE READ |
使用版本链 |
SERIALIZABLE |
加锁后访问记录 |
RC
和RR
都使用到了版本链,相同点是它们都需要借助ReadView
判断版本链中的哪个版本为当前事务可见,不同点的是它们ReadView
是否能重复生成。
ReadView
即读快照,其中主要存储了当前系统中还活跃的读写事务,这些活跃事务的trx_id
将放到一个名为m_ids
的列表中。
因此,在访问某条记录时,只需要按以下步骤即可判断记录的某个版本是否可见:
- 若被访问版本的
trx_id
属性值小于m_ids
列表中最小的事务 id,表明生成该版本的事务在生成ReadView
前已经提交,因此该版本可以被当前事务访问 - 若被访问版本的
trx_id
属性值大于m_ids
列表中最大的事务 id,表明生成该版本的事务在生成ReadView
后才生成,因此该版本不可以被当前事务访问 - 若被访问版本的
trx_id
属性值在m_ids
列表中最大的事务 id 和最小事务 id 之间,那就需要判断一下trx_id
属性值是不是在m_ids
列表中:- 若存在,说明创建
ReadView
时生成该版本的事务还是活跃的,该版本不可被访问 - 若不在,说明创建
ReadView
时生成该版本的事务已经被提交,该版本可以被访问
- 若存在,说明创建
如果某个版本的数据对当前事务不可见的话,那就顺着版本链找到下一个版本的数据,继续按照上边的步骤判断可见性,依此类推,直到版本链中的最后一个版本。
如果最后一个版本也不可见的话,那么就意味着该条记录对该事务不可见,查询结果就不包含该记录。
在MySQL
中,READ COMMITTED
和REPEATABLE READ
隔离级别的的一个非常大的区别就是它们生成ReadView
的时机不同,我们来看一下。
RC 场景
在READ COMMITTED
隔离级别下,每次读取数据前都将生成一个ReadView
。
我们先回忆一下,该隔离级别解决了什么并发问题。
脏读,没错,该隔离级别不允许脏读。
那么为何能解决脏读呢?
因为未提交(被回滚)的事务操作涉及的数据记录对其他事务不可见。
下面通过一个例子来解释一下:
比方说现在系统里有两个trx_id
分别为 100、200 的事务正在执行:
1 | # trx_id 100 |
1 | # trx_id 200 |
此刻,t
表中 id 为1 的记录得到的版本链表如下所示:
假设现在有一个使用READ COMMITTED
隔离级别的事务开始执行:
1 | # 使用 READ COMMITTED 隔离级别的事务 |
SELECT 1
的执行过程如下:
- 在执行
SELECT
语句时会先生成一个ReadView
,ReadView
的m_ids
列表内容为[100, 200]
- 然后从版本链中挑选可见的记录,从图中可以看出,最新版本的列
c
的内容是'张飞'
,该版本的trx_id
值为100
,存在于m_ids
列表,因此不符合可见性要求,根据roll_pointer
跳到下一个版本 - 下一个版本的列
c
的内容是'关羽'
,该版本的trx_id
值也为100
,也在m_ids
列表内,因此也不符合要求,继续跳到下一个版本。 - 下一个版本的列
c
的内容是'刘备'
,该版本的trx_id
值为80
,小于m_ids
列表中最小的trx_id
值100
,因此这个版本是符合要求的,那么最后返回给用户的版本就是这条列c
为'刘备'
的记录
这是事务 100 和 200 还未提交时的 SELECT
情况,现在我们仅仅提交一下事务 100 :
1 | # trx_id 100 |
然后在trx_id
为 200 的事务中更新一下t
表中 id 为 1 的记录:
1 | # trx_id 200 |
此刻,t
表中 id 为 1 记录的版本链就长这样:
然后再到刚才使用READ COMMITTED
隔离级别的事务中继续查找这个 id 为 1 的记录:
1 | # 使用 READ COMMITTED 隔离级别的事务 |
SELECT 2
的执行过程如下:
- 在执行
SELECT
语句时会先生成一个ReadView
,ReadView
的m_ids
列表的内容就是[200]
(trx_id
为100
的那个事务已经提交了,所以生成快照时就没有它了) - 然后从版本链中挑选可见的记录,从图中可以看出,最新版本的列
c
的内容是'诸葛亮'
,该版本的trx_id
值为200
,在m_ids
列表内,所以不符合可见性要求,根据roll_pointer
跳到下一个版本 - 下一个版本的列
c
的内容是'赵云'
,该版本的trx_id
值为200
,也在m_ids
列表内,所以也不符合要求,继续跳到下一个版本 - 下一个版本的列
c
的内容是'张飞'
,该版本的trx_id
值为100
,比m_ids
列表中最小的trx_id
值200
还要小,所以这个版本是符合要求的,最后返回给用户的版本就是这条列c
为'张飞'
的记录
以此类推,如果之后trx_id
为200
的记录也提交了,再此在使用READ COMMITTED
隔离级别的事务中查询表t
中id
值为1
的记录时,得到的结果就是'诸葛亮'
了,具体流程我们就不分析了。
总结一下就是:使用READ COMMITTED
隔离级别的事务在每次查询开始时都会生成一个独立的ReadView
。
RR 场景
在REPEATABLE READ
隔离级别下,只会在第一次执行查询语句时生成一个ReadView
,之后的查询不会重复生成。由于在同一个事务中,多次SELECT
操作只会生成一个ReadView
,所以自然就解决了不可重复读下的数据重复读取问题。
小结
MVCC 只在RC
、RR
两个隔离级别下工作,都通过ReadView
来判断事务版本信息,只会读取版本链中的最新已提交事务的 undo 日志。
MVCC 可以使不同事务的读-写、写-读操作并发执行,从而提升系统性能。
RC
、RR
两个隔离级别的一个很大不同就是生成ReadView
的时机不同:
RC
会在每一次进行普通SELECT
操作前都生成一个ReadView
RR
只在第一次进行普通SELECT
操作前生成一个ReadView
,之后的查询操作都重复该ReadView
其他两个隔离级别都和 MVCC 不兼容,因为RU
总是读取最新的数据行,而不是符合当前事务版本的数据行;SERIALIZABLE
则会对所有读取的行都加锁。
扩展——查询再插入再查询操作下的数据读取数
思考一个问题,如果一种表存在 4 条数据,我们开启一个事务后执行以下操作:
- ① 查询全表数据
- ② 插入一条数据
- ③ 查询全表数据
那么,第 ③ 步会查询到几条数据?
答案是 5 条数据。
具体过程如下:
1 | mysql> BEGIN; |
InnoDB 中的读
在 MySQL 中,支持三种类型的读语句:
- 一致性(无锁)读
- 半一致性读
- 锁定读
不同类型的读
一致性(无锁)读
一致性(无锁)读,亦称普通读,快照读,英文名:Consistent Read):指普通的SELECT
读语句,即在SELECT
语句末尾不加FOR UPDATE
或者LOCK IN SHARE MODE
的SELECT
语句。
普通读的执行方式是生成ReadView
直接利用 MVCC 机制来进行读取数据,并不会对记录进行加锁。
注意事项
注意哦,一致读取对某些 DDL 语句不起作用,比如说:
- 对
DROP TABLE
操作不起作用,因为 MySQL 无法使用已被丢弃的表,而且 InnoDB 会销毁表 - 对
ALTER TABLE
操作不起作用,因为 ALTER TABLE 操作会创建原始表的临时副本,并在创建临时副本时删除原始表。在事务中重新发出一致读时,新表中的行不可见,因为在事务快照时这些行还不存在。在这种情况下,事务会返回错误:ER_TABLE_DEF_CHANGED,”表定义已更改,请重试事务”
对于 INSERT INTO … 等子句中的选择,读取类型有所不同。select、update …(SELECT) 和 CREATE TABLE …SELECT 等子句中的选择类型有所不同,这些子句没有指定 FOR UPDATE 或 LOCK IN SHARE MODE:
- 默认情况下,InnoDB 会在这些语句中使用更强的锁,SELECT 部分的读取行为类似于 RC 下的 MVCC,在这种情况下,即使在同一个事务中,每次一致的读取都会设置并读取自己的新快照
- 要在这种情况下执行无锁读取,请启用
innodb_locks_unsafe_for_binlog
选项,并将事务的隔离级别设置为 RU、RC 或 RR,以避免对从所选表读取的记录设置锁
半一致性读
半一致性读(英文名:Semi-Consistent Read):这是一种夹在普通读和锁定读之间的一种读取方式。它只在READ COMMITTED
隔离级别下(或者在开启了innodb_locks_unsafe_for_binlog
系统变量的情况下)使用UPDATE
语句时才会使用。具体的含义就是当UPDATE
语句读取已经被其他事务加了锁的记录时,InnoDB
会将该记录的最新提交的版本读出来,然后判断该版本是否与UPDATE
语句中的WHERE
条件相匹配,如果不匹配则不对该记录加锁,从而跳到下一条记录;如果匹配则再次读取该记录并对其进行加锁。这样子处理只是为了让UPDATE
语句尽量少被别的语句阻塞。
注意:半一致性读只适用于对聚簇索引记录加锁的情况,并不适用于对二级索引记录加锁的情况。
另外,MySQL 官方文档并没有对半一致性读的单独说明,算是一致性读的一种特例。
锁定读
锁定读(英文名:Locking Read):指SELECT ... FOR UPDATE
或SELECT ... LOCK IN SHARE MODE
这种读语句,在事务读取记录之前,必须先获取该记录对应的锁。当然,获取什么类型的锁取决于当前事务的隔离级别、语句的执行计划、查询条件等因素。
注意事项
外层语句中的锁定读取子句不会锁定嵌套子查询中表的记录,除非在子查询中也指定了锁定读取子句。
例如,以下语句不会锁定表 t2 中的记录。
1 | SELECT * FROM t1 WHERE c1 = (SELECT c1 FROM t2) FOR UPDATE; |
因此,若要锁定表 t2 中的记录,请在子查询中也添加锁定读取子句:1
SELECT * FROM t1 WHERE c1 = (SELECT c1 FROM t2 FOR UPDATE) FOR UPDATE;
读在不同隔离级别的转换
对于SERIALIZABLE
隔离级别来说,如果系统变量autocommit
被设置为OFF
,那普通读的语句会转变为锁定读,和在普通的SELECT语
句后边加LOCK IN SHARE MODE
达成的效果一样。
疑问:RR 隔离级别下真的能避免幻读嘛?
我们在讨论问题之前,先回忆下幻读是什么。
幻读指某个事务突然看到了一个它以前没有见过的数据行。比如说,若某个事务刚执行完一条SELECT
语句就有另一个事务插入了一个新数据行,前一个事务再次执行同一条SELECT
语句时,就可能多看到一个新的数据行,这就是幻读。
在 RR 隔离级别下,一致性读看到是事务开始时的快照,即使其它事务插入了新行通常也是看不到的,所以在常见的场合可以避免幻读。 但是,对于执行锁定读、更新或者删除语句时,还是会看到数据库的最新状态,比如新插入的数据行,修改的数据行。
下面通过一个例子来复现这种情况:
SESSION 1:
1 | mysql> START TRANSACTION; |
SESSION 2:
1 | mysql> update tb1 set c1=101 where id =1; |
SESSION 1:
1 | mysql> select id,c1 from tb1 where id=1 LOCK IN SHARE MODE; |
上面 update 的行为违反了 RR 的承诺,看到了事务开始后其它事务的并发更新。这对应用开发需要特别注意,这种情况下其它数据库通常都是报错的。
扯了这么多,我们如何避免遇到这个并发更新问题呢?
答案很简单,修改时加WHERE
条件。
InnoDB 中的锁
在 MySQL InnoDB 存储引擎中,使用的锁存在以下类型:
- 共享
Shared
锁和排他Exclusive
锁 - 意向
Intention
锁 - 记录
Record
锁 - 间隙
Gap
锁 - 临键
Next-Key
锁 - 插入意向
Insert Intention
锁 - 自增
AUTO-INC
锁 - 空间索引的谓词锁(略)
共享锁和排他锁
InnoDB 实现了标准的行级锁,有两种类型的锁,共享(S)锁和排他(X)锁。
- 共享(S)锁:允许持有该锁的事务读取一条记录
- 排他(X)锁:允许持有该锁的事务更新或删除一条记录
如果事务 T1 持有 r 行的共享(S)锁,那么来自不同事务 T2 对 r 行的锁的请求将被如下处理:
- T2 对 S 锁的请求可以被立即批准。结果是,T1 和 T2 都持有 r 的 S 锁
- T2 对 X 锁的请求不能被立即批准
如果一个事务 T1 在行 r 上持有一个排他(X)锁,那么某个不同的事务 T2 对 r 的任一类型的锁的请求不能立即被批准。相反,事务 T2 必须等待事务 T1 释放其对 r 行的锁。
意向锁
InnoDB支持多粒度锁,允许行锁和表锁共存。例如,一个诸如LOCK TABLES ...WRITE
这样的语句在指定的表上取得了一个排他锁(一个X锁)。为了使多个粒度级别的锁切实可行,InnoDB 使用意向锁。
意向锁是表级别的锁,它表明事务以后需要对表中的某一行进行哪种类型的锁(共享或排他)。
在 MySQL 中,存在两种类型的意向锁:
- 意向共享锁(IS):表示事务打算对表中的个别行设置一个共享锁
- 意向排他锁(IX):表示一个事务打算在表中的个别行上设置一个排他锁
例如,SELECT ... LOCK IN SHARE MODE
设置了一个 IS 锁,SELECT ... FOR UPDATE
设置了一个 IX 锁。
意向锁协议如下:
- 在一个事务可以获得表内某行的共享锁之前,它必须首先获得该表的 IS 锁或更强的锁
- 在一个事务可以获得表中某行的排他锁之前,它必须首先获得该表的 IX 锁
表级的锁类型兼容性总结在下面的矩阵中。
X | IX | S | IS | |
---|---|---|---|---|
X | 冲突 | 冲突 | 冲突 | 冲突 |
IX | 冲突 | 兼容 | 冲突 | 兼容 |
S | 冲突 | 冲突 | 兼容 | 兼容 |
IS | 冲突 | 兼容 | 兼容 | 兼容 |
如果一个锁与现有的锁兼容,就会授予请求的事务,但如果与现有的锁冲突,就不会授予。一个事务会等待,直到与之冲突的现有锁被释放。如果一个锁请求与现有的锁冲突,并且不能被授予,因为它会导致死锁,那么就会发生一个错误。
除了全表请求(例如,LOCK TABLES … WRITE),意向锁不会阻止任何东西。意向锁的主要目的是显示有人正在锁定某一行,或将要锁定表中的某一行。
在SHOW ENGINE INNODB STATUS
和InnoDB
监控输出中,意向锁的事务数据显示类似于以下内容:
1 | TABLE LOCK table `test`.`t` trx id 10080 lock mode IX |
Q:为什么有意向锁?
为了提高并发锁操作的性能,啥意思呢?我们可以先讨论个问题。
如果一个数据行已被一个线程加锁处理中了,此时再来了一个新线程,那么新线程正常情况下会怎么执行自己的后续步骤呢?
答案很简单,尝试对数据行加锁,但是会发现数据行已被加锁,那么阻塞等待。
那么,经过以上一个过程,我们可以再思考一个问题:如果一个数据行已被加锁,后续线程可不可以直接阻塞等待,不要去尝试对数据行加锁,反正也抢不到锁,这么做可以减少一个步骤。
当然,可以,那么如何做呢?
需要一个标志来标明数据行已被加锁,而意向锁就可以作为这个标志。
记录锁
记录锁是对一个索引记录的锁。
例如,SELECT c1 FROM t WHERE c1 = 10 FOR UPDATE;
语句可以防止任何其他事务插入、更新或删除t.c1
的值为10
的记录。
记录锁总是锁定索引记录,即使一个表被定义为没有索引。在这种情况下,InnoDB 会创建一个隐藏的聚集索引,并使用这个索引来锁定记录。
记录锁的事务数据在SHOW ENGINE INNODB STATUS
和 InnoDB 监控输出中显示类似于以下内容:
1 | RECORD LOCKS space id 58 page no 3 n bits 72 index `PRIMARY` of table `test`.`t` |
间隙锁
间隙锁是对索引记录之间的间隙的锁,或者对第一个或最后一个索引记录之前的间隙的锁。
例如,SELECT c1 FROM t WHERE c1 BETWEEN 10 and 20 FOR UPDATE;
语句可以防止其他事务在列t.c1
中插入一个15
的值,无论该列中是否已经有这样的值,因为该范围中所有现有值之间的空隙被锁定。
一个间隙可能横跨一个索引值,多个索引值,甚至是空的。
间隙锁是性能和并发性之间权衡的一部分,并且只在一些事务隔离级别中使用。
对于使用唯一索引锁定行以搜索唯一行的语句,不需要间隙锁。(这不包括搜索条件只包括多列的某些列的情况。
在 locking reads (SELECT with FOR UPDATE or LOCK IN SHARE MODE),UPDATE 和 DELETE 时,锁定取决于 SQL 语句是否是使用具有唯一搜索条件的唯一索引,或者范围类型的搜索条件:
- 对于具有唯一搜索条件的唯一索引,InnoDB 只锁定找到的索引记录,不锁定之前的间隙
- 对于其他搜索条件,InnoDB 锁定扫描的索引范围,使用间隙锁或临键锁来阻止插入由其他 Session 填补范围内覆盖的间隙
插入意向锁
插入意向锁是一种由 INSERT 操作在行插入前设置的间隙锁。这种锁发出了插入意向的信号,如果多个事务在同一索引间隙中插入的位置不一样,就不需要互相等待。
假设有数值为 4 和 7 的索引记录。分别试图插入值为 5 和 6 的事务,在获得插入行的排他锁之前,各自用插入意向锁(间隙锁)锁定了 4 和 7 之间的间隙,但是由于这些行是不冲突的,所以不会互相阻塞。
下面的例子演示了一个事务在获得被插入记录的排他锁之前,采取插入意向锁。这个例子涉及两个客户,A 和 B。
客户端 A 创建了一个包含两个索引记录(90 和 102)的表,然后启动一个事务,对 ID 大于 100 的索引记录加了一个排他锁。该排他锁包括 102 号记录前的间隙锁:
1 | mysql> CREATE TABLE child (id int(11) NOT NULL, PRIMARY KEY(id)) ENGINE=InnoDB; |
客户端 B 开始一个事务,向缺口插入一条记录。该事务在等待获得排他锁的同时,采取了一个插入意向锁。
1 | mysql> START TRANSACTION; |
在 SHOW ENGINE INNODB STATUS 和 InnoDB 监控输出中,插入意向锁的事务数据显示类似于以下内容:
1 | RECORD LOCKS space id 31 page no 3 n bits 72 index `PRIMARY` of table `test`.`child` |
AUTO-INC 锁
AUTO-INC 锁是一个特殊的表级锁,由插入有 AUTO_INCREMENT 列的表的事务使用。在最简单的情况下,如果一个事务正在向表中插入数值,任何其他事务必须等待向该表进行自己的插入,这样第一个事务插入的行就会得到连续的主键值。
innodb_autoinc_lock_mode
变量控制用于自动增量锁定的算法。它允许你选择如何在可预测的自动递增值序列和插入操作的最大并发性之间进行交换。
表锁与行锁
对于前面涉及的锁而言,从锁的粒度维度,可以分成表锁与行锁两大类:
类别 | 说明 | 特点 |
---|---|---|
表锁 | 对查询的整张表加锁 | 开销小,加锁快;不会出现死锁; 锁定力度大,发生锁冲突概率高,并发度最低 |
行锁 | 对查询出的行加锁 | 开销大,加锁慢;会出现死锁; 锁定粒度小,发生锁冲突的概率低,并发度高 |
当然,不同的存储引擎支持的锁粒度不同:
- InnoDB 既支持行锁又支持表锁
- MyISAM 只支持表锁
扩展——封锁粒度与封锁协议
数据库中为了实现并发控制而采用封锁技术。封锁对象的大小称为封锁粒度( Granularity )。
封锁的对象可以是逻辑单元,亦可以是物理单元。
以关系数据库为例,封锁对象可以为以下逻辑单元:
- 属性值
- 属性值的集合
- 元组
- 关系
- 索引项
- 整个索引项
- 甚至整个数据库
封锁对象亦可以为以下物理单元:
- 页(数据页或索引页)
- 物理记录等
在运用 X 锁和 S 锁对数据对象加锁时,还需要约定一些规则。比如何时申请 X 锁 或 S 锁、持锁时间多久、何时释放等规则,这些规则被称为为封锁协议( Locking Protocol )。
对封锁方式规定的不同规则,形成了不同的封锁协议,而不同的封锁协议又对应不同的隔离级别(注意哦
:封锁协议和隔离级别并不是严格对应的)。
参考
- MySQL 官方文档—— Innodb Lock
- MySQL 是怎样运行的:从根儿上理解 MySQL
- Paul DuBois. MySQL 技术内幕 [M]. 人民邮电出版社, 2011
文章信息
时间 | 说明 |
---|---|
2022-02-18 | 初稿 |